#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <errno.h>
#include <unistd.h>
#include <syslog.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdarg.h>


#define MAXLINE 200


static void err_doit(int errnoflag, int error, const char *fmt, va_list ap) {
    char buf[MAXLINE];

    // 将格式化串fmt (参数ap) 转换成字符串存放到buf
    vsnprintf(buf, MAXLINE - 1, fmt, ap);
    if (errnoflag) {
        // snprintf最后一个参数是const char*, vsnprintf最后一个参数是va_list, 功能一样
        snprintf(buf + strlen(buf), MAXLINE - strlen(buf) - 1, ": %s", strerror(error));    
    }

    strcat(buf, "\n"); // buf末尾粘贴 "\n", 并以'\0'结束
    fflush(stdout); // 以防stdout, stderr是相同设备, 先冲刷stdout
    fputs(buf, stderr);
    fflush(NULL); // 冲刷所有stdio输出流
}


/**
 * 与系统调用相关的致命错误
 * 打印消息和终止程序
 */
static void err_quit (const char *fmt, ...) {
    va_list ap;

    // va_start, va_end 配对获取可变参数..., 存放到va_list ap中
    va_start(ap, fmt);
    err_doit(0, 0, fmt, ap);
    va_end(ap);

    exit(1);
}


/**
* 将一个进程转换为守护进程
* 步骤:
* 1. 调用umask设置创建文件权限的umask
* 2. 调用fork, 让父进程退出, 子进程成为孤儿进程
* 3. 调用setsid, 创建新会话, 子进程成为新会话首进程以及进进程组组长, 断开控制终端
* 4. 再次调用fork, 并让父进程退出, 子进程不是会话首进程(防止再次获得控制终端)
* 5. 将当前工作目录修改为根目录, 防止无法卸载目录
* 6. 关闭不需要的文件描述符
* 7. 打开/dev/null, 使其具有文件描述符0,1,2
*/
void daemonize(const char *cmd) {
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;
    struct sigaction sa;

    // 1. 调用umask修改创建文件权限的umask
    umask(0); 

    if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
        // <=> sysconf(_SC_OPEN_MAX);
        err_quit("%s: getrlimit error", cmd);
    }

    // 2. 调用fork, 父进程退出, 子进程称为孤儿被init进程收养
    if ((pid = fork()) < 0)
        err_quit("%s: fork error", cmd);
    else if (pid > 0) { // 父进程
        exit(0);
    }

    // 3. 调用setsit, 创建新会话, 子进程成为首进程
    setsid();

    // 阻塞SIGHUP信号
    /* 发送SIGHUP信号情形:
    1)终端关闭时, 信号被发送到session首进程, 以及作为job提交的进程(shell 以&方式运行的进程)
    2)session首进程退出时, 该信号被发送到同session的所有前台进程
    3)若父进程退出, 导致进程组成为孤儿进程组, 且该进程组中有进程处于停止状态(收到SIGSTOP信号或SIGSTP), 该信号会被发送到进程组每个成员
    */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if (sigaction(SIGHUP, &sa, NULL) < 0)
        err_quit("%s: can't ignore SIGHUP", cmd);

    // 4. 再次调用fork
    if ((pid = fork()) < 0)
        err_quit("%s: fork error", cmd);
    else if (pid > 0)
        exit(0);

    // 5. 改变当前工作目录为根目录 
    if (chdir("/") < 0)
        err_quit("%s: can't change directory to /", cmd);

    // 6. 关闭不需要的文件描述符
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (i = 0; i < rl.rlim_max; ++i)
        close(i);

    // 7. 附加文件描述符0,1,2到 /dev/null
    // 因为前面已经关闭了所有文件描述符, 因此重新open, dup得到的文件描述符是递增的
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);

    // 初始化日志文件
    openlog(cmd, LOG_CONS, LOG_DAEMON); // 打开一个到系统日志的连接
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
        exit(1);
    }
}

